import os
import sys
import json
import pandas as pd
from PyQt5 import QtCore, QtGui
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import Qt, QThread, pyqtSignal  # 确保导入 Qt
from PyQt5.QtCore import pyqtSlot
import datetime
import tushare as ts  # 金融接口

from model.pandasModel import PandasModel
from view.Ui_mainWindow import Ui_MainWindow
from controller.sys_config import SysConfig
from controller.plate_config import PlateConfig
from controller.kline_plot import KLineWidget

from predict.SVM.paramPredict import ParamPredicter
from predict.ARMA.arma_trainer import ArmaTrainer

class LoadDataThread(QThread):
    finished = pyqtSignal(bool)  # 信号，用于通知主线程加载是否成功

    def __init__(self, wg, stock_code, period):
        super().__init__()
        self.wg = wg
        self.stock_code = stock_code
        self.period = period

    def run(self):
        try:
            self.wg.loadData(self.wg.dataGetter.getData(self.stock_code, self.period))
            self.wg.refreshAll()
            self.finished.emit(True)  # 加载成功，发射信号
        except Exception as e:
            print(f"加载数据失败: {e}")
            self.finished.emit(False)  # 加载失败，发射信号

# 通过ts金融接口获取股票数据df
def getStockData():
    ts.set_token('2848714833599a72886fe769d6ef5a79c03176076f9c15c7b0273f64')
    pro = ts.pro_api()
    data = pro.stock_basic(exchange='', list_status='L', fields='ts_code,symbol,name,market,cnspell')
    today = datetime.datetime.today().strftime('%Y%m%d')  # 今天日期
    df = pro.daily(trade_date=today)  # 今日数据
    # 自动往前获取数据
    for i in range(10):
        if not df.empty:
            break
        day = (datetime.date.today() + datetime.timedelta(-i)).strftime('%Y%m%d')
        df = pro.daily(trade_date=day)

    # 初始化表格数据
    stockData = pd.merge(data, df)[
        ['ts_code', 'symbol', 'name', 'pct_chg', 'close', 'change', 'open', 'high', 'low', 'vol', 'amount', 'market', 'cnspell']
    ].sort_index()
    stockData['cnspell'] = stockData['cnspell'].str.upper()  # 将拼音缩写变为大写
    return stockData


# 从文件中读取数据,如果文件不存在或是非最新数据,则会更新数据,并写入文件
def initData(filePath):
    if os.path.exists(filePath):  # 文件存在,则判断是否是最新数据
        mtime = datetime.datetime.fromtimestamp(os.path.getmtime(filePath)).strftime("%Y%m%d")  # 文件上次修改日期
        today = datetime.datetime.today().strftime('%Y%m%d')  # 今天日期
        # if mtime == today:  # 是最新数据
        #     # 如果不设置dtype=str,会将数字字符串将数字读,自动删除前面的0, 比如'000001'变成1
        #     # 但是如果设置了又会有别的BUG,此处用另一个解决方案
        #     stockData = pd.read_csv(filePath)
        #     # 重新单独读取symbol那一列,并指定dtype=str
        #     stockData['symbol'] = pd.read_csv(filePath, usecols=[1], dtype=str)
        # else:  # 非最新数据,需要更新数据,并写入文件
        #     stockData = getStockData()
        #     stockData.to_csv(filePath, encoding="utf_8_sig", index=False)

        # 但是如果设置了又会有别的BUG,此处用另一个解决方案
        stockData = pd.read_csv(filePath)
        # 重新单独读取symbol那一列,并指定dtype=str
        stockData['symbol'] = pd.read_csv(filePath, usecols=[1], dtype=str)
    else:  # 文件不存在或今天还没更新,则生成数据,并写入文件
        stockData = getStockData()
        stockData.to_csv(filePath, encoding="utf_8_sig", index=False)
    return stockData


# 通过股票代码获取公司名称
# def getName(stoke_code):
#     data = ts.pro_api().query('stock_basic', fields='symbol,name')
#     company_name = list(data.loc[data['symbol'] == stoke_code].name)[0]
#     return company_name

# def getName(stock_code):
#     try:
#         df = ts.get_stock_basics()
#         # 查找匹配的股票代码
#         stock_info = df[df.index == stock_code].iloc[0]
#         return stock_info['name']
#     except Exception as e:
#         print(f'获取股票名称失败: {e}')
#         return None
import akshare as ak
# 删除旧的tushare版本，保留akshare版本
def getName(stock_code):
    try:
        # 转换为字符串并补齐6位
        code_str = str(stock_code).zfill(6)
        # 使用akshare接口获取股票信息
        df = ak.stock_individual_info_em(symbol=code_str)
        # 添加列名存在性检查
        if 'item' in df.columns and 'value' in df.columns:
            name = df.loc[df['item'] == '股票简称', 'value'].values[0]
            return name if pd.notnull(name) else "未知股票"
        return "字段缺失"
    except Exception as e:
        print(f'获取股票名称失败: {e}')
        return "查询失败"



# 通过股票代码获取ts代码
def getTsCode(stoke_code):
    data = ts.pro_api().query('stock_basic', fields='ts_code,symbol,name')
    # print(data)
    ts_code = list(data.loc[data['symbol'] == stoke_code].ts_code)[0]
    return ts_code


class MainWindow(QMainWindow, Ui_MainWindow):  # 多重继承QMainWindow和Ui_MainWindow
    def __init__(self):
        super(MainWindow, self).__init__()  # 先调用父类QMainWindow的初始化方法
        self.setupUi(self)  # 再调用setupUi方法

        self.setMinimumSize(1250, 600)

        # 设置图标
        icon = QtGui.QIcon()
        icon.addPixmap(QtGui.QPixmap("resources/stock_icon.ico"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
        self.setWindowIcon(icon)

        # palette = QtGui.QPalette()
        # palette.setColor(self.backgroundRole(), QColor(192, 253, 123))   # 设置背景颜色
        # self.setPalette(palette)

        # 表格样式
        self.stockView.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)  # 列宽自适应充满表格
        font = QFont('微软雅黑', 11)
        font.setBold(True)
        self.stockView.horizontalHeader().setFont(font)  # 设置表头字体
        self.stockView.setFont(QFont('微软雅黑', 8))  # 设置表身字体
        self.stockView.setEditTriggers(QAbstractItemView.NoEditTriggers)  # 设置表格不可更改
        self.stockView.setSelectionBehavior(QAbstractItemView.SelectRows)  # 设置不可选择单个单元格,只可选择一行
        self.stockView.setSelectionMode(QAbstractItemView.SingleSelection)  # 设置为只能选择单行,不多同时选择多行
        self.stockView.verticalHeader().setDefaultSectionSize(25)  # 设置默认行高
        # self.stockView.horizontalHeader().setDefaultSectionSize(200)  # 设置默认列宽
        self.stockView.setShowGrid(True)  # 设置有网格

        self.stockData = initData("data/stockData.csv")  # 初始化数据
        # 初始化常规板块数据
        self.plate1 = self.stockData[self.stockData['market'] == "主板"]
        self.plate2 = self.stockData[self.stockData['market'] == "中小板"]
        self.plate3 = self.stockData[self.stockData['market'] == "创业板"]
        self.plate4 = self.stockData[self.stockData['market'] == "科创板"]
        self.plate5 = pd.DataFrame()  # 自选股的初始化比较复杂,点击时候再写入数据

        # 表格初始化显示股票列表
        self.headers = ['TS代码', '股市代码', '名称', '涨幅%', '收盘', '涨跌', '开盘', '最高价', '最低价', '成交量(手)', '成交额', '市场', '拼音缩写']
        self.model = PandasModel(self.plate1, self.headers)  # 默认展示主板界面
        self.stockView.setModel(self.model)  # 关联QTableView控件和Model
        self.setBtnStyle(self.plate1Btn)
        self.stockView.setColumnHidden(self.stockData.columns.get_loc('ts_code'), True)  # 隐藏 ts_code 列
        self.stockView.setColumnHidden(self.stockData.columns.get_loc('market'), True)  # 隐藏 market 列
        self.stockView.setColumnHidden(self.stockData.columns.get_loc('cnspell'), True)  # 隐藏 cnspell 列

        # 设置表格双击事件
        self.stockView.doubleClicked.connect(self.stockView_double_clicked)

        # 隐藏TabWidget的标签标题
        self.stockWidget.tabBar().hide()  # 隐藏stockWidget的标签标题
        self.trainWidget.tabBar().hide()  # 隐藏trainWidget的标签标题
        self.trainShowWidget.tabBar().hide()
        # 为【股票】的【技术分析】创建一个新的Tab页
        # self.dataGetter = GetData()  # 用来获取数据的对象
        self.wg = KLineWidget(self)
        self.stockWidget.addTab(self.wg, 'K线图')  # 将self.wg作为新的Tab页追加到stockWidget中
        # 为【训练】的【获取数据】创建一个新的Tab页
        self.wg2 = KLineWidget(self)
        self.wg2.btnReture.setVisible(False)  # 【训练】无返回按钮
        self.trainShowWidget.addTab(self.wg2, 'K线图')

        # 【训练】界面的参数
        self.paramPredicter = None
        self.ppData = None
        self.test_size = 0.05

        self.armaTrainer = None

        self.trainDayNum = 0

        self.welcomeLabel.setStyleSheet("background-color:black;color:white")  # "欢迎使用训练功能。" 的样式

        # 默认进入【股票】页面
        self.mainWidget.setCurrentIndex(0)

    # 【菜单栏】的事件 ######################################################
    @pyqtSlot()
    def on_actSysConfig_triggered(self):
        QMessageBox.information(self, '提示信息', '系统配置功能。')

    @pyqtSlot()
    def on_actSysConfig_triggered(self):
        w = SysConfig()
        w.exec_()

    @pyqtSlot()
    def on_actPlateConfig_triggered(self):
        w = PlateConfig()
        w.exec_()

    @pyqtSlot()
    def on_actExit_triggered(self):
        self.close()

    # 【股票】标签页面的事件 ######################################################
    @pyqtSlot()
    def on_btnTech_clicked(self):  # 技术分析
        index = self.stockView.currentIndex()  # 当前选中的行的索引
        table_row = index.row()  # 根据索引获取行序号
        if table_row == -1:
            return
        stock_code = index.model().index(table_row, 1).data()  # 股票代码
        stock_name = index.model().index(table_row, 2).data()  # 股票名
        self.wg.stock_code, self.wg.stock_name = stock_code, stock_name  # 给K线图对象设置属性
        self.wg.stockLabel.setText("[%s - %s]" % (stock_code, stock_name))  # 设置K线图界面显示的股票信息
        self.wg.setCrosshairInfo(stock_name, 'D')  # 默认展示每日行情数据,所以为D
        self.wg.loadData(self.wg.dataGetter.getData(stock_code, 'D'))
        self.wg.refreshAll()
        self.stockWidget.setCurrentIndex(1)  # 切换标签

    # 表格行的双击事件
    # def stockView_double_clicked(self, index):
    #     table_row = index.row()  # 点击的表格行
    #     if table_row == -1:
    #         return
    #     stock_code = index.model().index(table_row, 1).data()  # 股票代码
    #     stock_name = index.model().index(table_row, 2).data()  # 股票名
    #     self.wg.stock_code, self.wg.stock_name = stock_code, stock_name
    #     self.wg.stockLabel.setText("[%s - %s]" % (stock_code, stock_name))  # 设置K线图界面显示的股票信息
    #     self.wg.setCrosshairInfo(stock_name, 'D')  # 默认展示每日行情数据,所以为D
    #
    #     msg_box = QMessageBox(self)
    #     msg_box.setWindowTitle("加载中")
    #     msg_box.setText("正在加载数据，请稍候...")
    #     msg_box.setStandardButtons(QMessageBox.Cancel)  # 添加取消按钮
    #     msg_box.setIcon(QMessageBox.Information)
    #     msg_box.show()
    #
    #
    #
    #     # 模拟加载数据
    #     try:
    #         self.wg.loadData(self.wg.dataGetter.getData(stock_code, 'D'))
    #         self.wg.refreshAll()
    #         self.stockWidget.setCurrentIndex(1)  # 切换标签
    #     except Exception as e:
    #         print(f"加载数据失败: {e}")
    #         QMessageBox.information(self, "提示", "加载数据失败，请稍后重试。")
    #     finally:
    #         msg_box.close()  # 关闭消息框

    # 在 MainWindow 类中修改 stockView_double_clicked 方法
    def stockView_double_clicked(self, index):
        table_row = index.row()  # 点击的表格行
        if table_row == -1:
            return
        stock_code = index.model().index(table_row, 1).data()  # 股票代码
        stock_name = index.model().index(table_row, 2).data()  # 股票名
        self.wg.stock_code, self.wg.stock_name = stock_code, stock_name
        self.wg.stockLabel.setText("[%s - %s]" % (stock_code, stock_name))  # 设置K线图界面显示的股票信息
        self.wg.setCrosshairInfo(stock_name, 'D')  # 默认展示每日行情数据,所以为D

        # 显示加载中的消息框
        self.msg_box = QMessageBox(self)
        self.msg_box.setWindowTitle("加载中")
        self.msg_box.setText("正在加载数据，请稍候...")
        self.msg_box.setStandardButtons(QMessageBox.Cancel)  # 添加取消按钮
        self.msg_box.setIcon(QMessageBox.Information)
        self.msg_box.show()

        # 启动异步加载数据的线程
        self.load_thread = LoadDataThread(self.wg, stock_code, 'D')
        self.load_thread.finished.connect(self.on_load_finished)
        self.load_thread.start()

    # 添加 on_load_finished 方法
    def on_load_finished(self, success):
        self.msg_box.close()  # 关闭消息框
        if success:
            self.stockWidget.setCurrentIndex(1)  # 切换标签
        else:
            QMessageBox.information(self, "提示", "加载数据失败，请稍后重试。")

    # 搜索功能
    def on_search(self, str):
        searchText = self.searchInput.text()
        if self.find(searchText, self.stockData.columns.get_loc('ts_code')):
            pass
        elif self.find(searchText, self.stockData.columns.get_loc('name')):
            pass
        elif self.find(searchText, self.stockData.columns.get_loc('cnspell')):
            pass

    # 在column列搜索text文本
    def find(self, text, column=0):
        model = self.stockView.model()
        start = model.index(0, column)
        matches = model.match(
            start, QtCore.Qt.DisplayRole,
            text, 1, QtCore.Qt.MatchContains  # 搜索模式为包含
        )
        if matches:
            index = matches[0]  # index.row(), index.column()
            self.stockView.selectRow(index.row())
            return True
        return False

    # 设置选中某个板块时,按钮的样式
    def setBtnStyle(self, plateBtn):
        self.plate1Btn.setEnabled(True)
        self.plate2Btn.setEnabled(True)
        self.plate3Btn.setEnabled(True)
        self.plate4Btn.setEnabled(True)
        self.plate5Btn.setEnabled(True)
        self.plate6Btn.setEnabled(True)
        plateBtn.setEnabled(False)

    @pyqtSlot()
    def on_plate1Btn_clicked(self):  # 主板
        self.model = PandasModel(self.plate1, self.headers)
        self.stockView.setModel(self.model)
        self.setBtnStyle(self.plate1Btn)

    @pyqtSlot()
    def on_plate2Btn_clicked(self):  # 中小板
        self.model = PandasModel(self.plate2, self.headers)
        self.stockView.setModel(self.model)
        self.setBtnStyle(self.plate2Btn)

    @pyqtSlot()
    def on_plate3Btn_clicked(self):  # 创业板
        self.model = PandasModel(self.plate3, self.headers)
        self.stockView.setModel(self.model)
        self.setBtnStyle(self.plate3Btn)

    @pyqtSlot()
    def on_plate4Btn_clicked(self):  # 科创板
        self.model = PandasModel(self.plate4, self.headers)
        self.stockView.setModel(self.model)
        self.setBtnStyle(self.plate4Btn)

    @pyqtSlot()
    def on_plate5Btn_clicked(self):
        self.plate5.drop(self.plate5.index, inplace=True)  # 清空数据
        # 初始化自选股板块
        jsonFilePath = 'data/plate.json'
        with open(jsonFilePath, 'r', encoding='utf-8') as f:
            json_data = json.loads(f.read())
        for item in json_data[0]['stocks']:
            # self.plate5 = self.plate5.append(self.stockData[self.stockData['symbol'] == item[:6]])
            self.plate5 = pd.concat([self.plate5, self.stockData[self.stockData['symbol'] == item[:6]]])
        self.model = PandasModel(self.plate5, self.headers)
        self.stockView.setModel(self.model)
        self.setBtnStyle(self.plate5Btn)

    # 【训练】标签页面的事件 ######################################################
    @pyqtSlot()
    def on_dataButton_clicked(self):  # 训练页面, 获取数据
        stock_code = self.stockCodeInput.text()  # 股票代码
        try:
            if stock_code != None:
                print("股票代码", stock_code)
                stock_name = getName(stock_code)
                print("股票名称", stock_name)
                self.wg2.stockLabel.setText("[%s - %s]" % (stock_code, stock_name))  # 设置K线图界面显示的股票信息
                self.wg2.stock_code, self.wg2.stock_name = stock_code, stock_name
                self.wg2.setCrosshairInfo(stock_name, 'D')  # 默认展示每日行情数据,所以为D
                self.wg2.loadData(self.wg2.dataGetter.getData(stock_code, 'D'))
                self.wg2.refreshAll()
                self.trainShowWidget.setCurrentIndex(1)  # 切换标签

                self.trainConfig.setEnabled(True)
        except:
            QMessageBox.information(self, '提示信息', '获取股票信息失败。')

    # 开始训练按钮,创建训练对象,训练相关面板可用
    @pyqtSlot()
    def on_beginTrainBtn_clicked(self):
        self.beginTrainBtn.setEnabled(False)  # 开始训练不可用
        self.endTrainBtn.setEnabled(True)  # 停止训练可用
        # 训练配置面板不可用
        self.startDateEdit.setEnabled(False)
        self.endDateEdit.setEnabled(False)
        self.splitInput.setEnabled(False)
        # 其余面板可用,并设置初始值
        if self.splitInput.text() == "":
            self.splitInput.setText("0.05")
        self.trainInfo.setEnabled(True)  # 训练信息面板可用
        self.predictOption.setEnabled(True)  # 预测选项面板可用
        self.predictOpen.setEnabled(True)
        self.predictClose.setEnabled(True)
        self.predictHigh.setEnabled(True)
        self.predictLow.setEnabled(True)
        self.paramValue.setEnabled(True)  # 参数评估面板可用

        # 获取界面信息
        begin_date = self.startDateEdit.text().replace("-", "")
        # print(begin_date)
        end_date = self.endDateEdit.text().replace("-", "")
        # print(end_date)
        self.test_size = float(self.splitInput.text())

        # 创建训练对象, 生成训练数据
        self.paramPredicter = ParamPredicter()
        self.ppData = self.paramPredicter.initData(
            code=getTsCode(self.stockCodeInput.text()),  # 此处要处理一下
            start=begin_date,
            end=end_date)

    # 停止训练按钮
    @pyqtSlot()
    def on_endTrainBtn_clicked(self):
        self.beginTrainBtn.setEnabled(True)  # 开始训练可用
        self.endTrainBtn.setEnabled(False)  # 结束训练不可用
        # 训练配置面板可用
        self.startDateEdit.setEnabled(True)
        self.endDateEdit.setEnabled(True)
        self.splitInput.setEnabled(True)
        # 其余面板不可用
        self.trainInfo.setEnabled(False)
        self.predictOption.setEnabled(False)
        self.paramValue.setEnabled(False)
        self.splitInput.setText("")

        begin_date = self.startDateEdit.text().replace("-", "")
        end_date = self.endDateEdit.text().replace("-", "")

        self.trainDayNum = 0
        self.trainDay.setText(str(self.trainDayNum))

    # 下一K线按钮
    @pyqtSlot()
    def on_nextKLineBtn_clicked(self):
        self.armaTrainer = ArmaTrainer()
        code = getTsCode(self.stockCodeInput.text())
        data = self.armaTrainer.initData(code, start="20180901")
        nextData = self.armaTrainer.predictNext(data)
        print(nextData)
        self.wg2.loadData(self.wg2.dataGetter.getData(self.wg2.stock_code, 'D', data=nextData))
        self.wg2.refreshAll()

        self.trainDayNum = self.trainDayNum + 1
        self.trainDay.setText(str(self.trainDayNum))


    # 预测开盘价
    @pyqtSlot()
    def on_predictOpen_clicked(self):
        self.paramPredicter.predict(
            data=self.ppData,
            param="open",
            test_size=self.test_size)

    # 预测收盘价
    @pyqtSlot()
    def on_predictClose_clicked(self):
        self.paramPredicter.predict(
            data=self.ppData,
            param="close",
            test_size=self.test_size)

    # 预测最高价
    @pyqtSlot()
    def on_predictHigh_clicked(self):
        self.paramPredicter.predict(
            data=self.ppData,
            param="high",
            test_size=self.test_size)

    # 预测最低价
    @pyqtSlot()
    def on_predictLow_clicked(self):
        self.paramPredicter.predict(
            data=self.ppData,
            param="low",
            test_size=self.test_size)

    # 预测股票走势
    @pyqtSlot()
    def on_predictTrend_clicked(self):
        self.paramPredicter.predictTrend(data=self.ppData)

    # 公共方法 ######################################################
    def returnToList(self):
        self.stockWidget.setCurrentIndex(0)

    # 处理关闭时的提示  
    def closeEvent(self, event):
        reply = QMessageBox.question(self, '退出', "确定要退出吗?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
        if reply == QMessageBox.Yes:
            event.accept()
        else:
            event.ignore()



if __name__ == '__main__':
    app = QApplication(sys.argv)
    # app.setStyle(QStyleFactory.create("Fusion"))
    ui = MainWindow()
    ui.show()
    sys.exit(app.exec_())